import java.io.*;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.stream.Collectors;

public class MyDedup {
    public static void usage() {
        System.out.println("Usage: ");
        System.out.println(" java -cp .:./lib/* MyDedup upload <min_chunk> <avg_chunk> <max_chunk> <d> <file_to_upload> <local|azure>");
        System.out.println(" java -cp .:./lib/* MyDedup download <file_to_download> <local_file_name> <local|azure>");
        System.out.println(" java -cp .:./lib/* MyDedup delete <file_to_delete> <local|azure>");
    }

    public static void main(String[] args) {
        MyDedup myDedup = new MyDedup();
        myDedup.realEntry(args);
//        MyDedupIndex myDedupIndex = MyDedupIndex.loadIndex();
//        int minChunk = 4096;
//        int avgChunk = 8192;
//        int maxChunk = 16384;
//        int d = 257;
//        List<String> testFiles = myDedup.writeTestFile();
//        for (String pathName : testFiles) {
//            String downloadedPathName = pathName+"_";
//            myDedup.upload(minChunk, avgChunk, maxChunk, d, pathName, myDedupIndex);
//            myDedup.download(pathName, downloadedPathName, myDedupIndex);
//            myDedup.checkUploadDown(pathName,downloadedPathName);
//            myDedup.delete(pathName, myDedupIndex);
//        }
//        myDedupIndex.saveIndex();

    }

    static boolean isPowerOfTwo(int n) {
        return (int) (Math.ceil((Math.log(n) / Math.log(2))))
                == (int) (Math.floor(((Math.log(n) / Math.log(2)))));
    }

    int powMod(int x, int y, int z) {
        int result = 1 % z;
        x %= z;
        while (y != 0) {
            if ((y & 1) == 1) {
                result = (result * x) % z;
            }
            x = (x * x) % z;
            y >>= 1;
        }
        return result;
    }

    void realEntry(String[] args) {
        if (args.length < 1) {
            usage();
            System.exit(1);
        }
        MyDedupIndex myDedupIndex = MyDedupIndex.loadIndex();
        String operation = args[0];
        try {
            if (operation.equals("upload")) {
                if (args.length != 7) {
                    usage();
                    System.exit(1);
                }
                int minChunk = Integer.valueOf(args[1]);
                int avgChunk = Integer.valueOf(args[2]);
                int maxChunk = Integer.valueOf(args[3]);
                int d = Integer.valueOf(args[4]);
                String fileToUpload = args[5];
                String mode = args[6];  // local or azure
                myDedupIndex.setContainerMode(mode);
                if (!isPowerOfTwo(minChunk) || !isPowerOfTwo(avgChunk) || !isPowerOfTwo(maxChunk)) {
                    System.out.println("Chunk siZe is s required to be a power of 2");
                    System.exit(1);
                }
                upload(minChunk,avgChunk,maxChunk,d, fileToUpload,myDedupIndex);
            } else if (operation.equals("download")) {
                if (args.length < 3) {
                    usage();
                    System.exit(1);
                }
                String remoteFileName = args[1];
                String localFileName = args[2];
                String mode = args[3];  // local or azure
                myDedupIndex.setContainerMode(mode);
                download(remoteFileName, localFileName,myDedupIndex);

            } else if (operation.equals("delete")) {
                if (args.length < 2) {
                    usage();
                    System.exit(1);
                }
                String fileName = args[1];
                String mode = args[2];  // local or azure
                myDedupIndex.setContainerMode(mode);
                delete(fileName,myDedupIndex);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            myDedupIndex.saveIndex();
        }
    }

    void upload(int windowSize, int modulus, int maxChunkSize, int d, String filePath, MyDedupIndex myDedupIndex) {
        if (myDedupIndex.containFile(filePath)) {
            System.out.println("Duplicated file with same filePath.");
            return;
        }
        chunking(windowSize, modulus, maxChunkSize, d, filePath, myDedupIndex);
        myDedupIndex.printStatics();
    }

    void chunking(int m, int q, int maxChunkSize, int d, String filePath, MyDedupIndex myDedupIndex) {
        try (InputStream inputStream = new FileInputStream(filePath)) {
            int n = 0;  // has read size
            int wantCount = m;  // how many bytes wants to read
            int offset = 0; // target buff offset
            byte[] b = new byte[maxChunkSize];  // buffer with max size
            int len = 0;
            int prevRFP = -1;  // previous RFP
            while ((n = inputStream.read(b, offset, wantCount)) != -1) {
                boolean needChunk = false;
                len += n;
                if (n < wantCount) {  // last chunk
                    myDedupIndex.addLastChunk(filePath, b, len);
                    break;
                }
                if (len == m || prevRFP == -1) {  // s=0 in formula
                    int p0 = 0;
                    for (int i = 0; i < m; i++) {
                        p0 += ((b[i] % q) * powMod(d,m-1-i,q)) % q;
                    }
                    p0 = Math.floorMod(p0, q);  // make sure positive remainder
                    prevRFP = p0;
                } else {
                    if (len >= maxChunkSize) {  // reached max size
                        needChunk = true;
                    } else {  // s>0 in formula
                        int tsm = b[len - 1];
                        int tp = b[len - 1 - m];
                        int rfp = Math.floorMod((d % q) * (prevRFP % q - powMod(d, m - 1, q) * (tp % q)) + tsm % q, q);
//                        int rfp = Math.floorMod((int) ((d % q) * (prevRFP % q - (Math.pow(d, m - 1) % q) * (tp % q)) + tsm % q), q);
                        if (rfp == 0) {  // RFP & mask = 0
                            needChunk = true;
                        } else {
//                            System.out.println("I doubt have no rfp!=0");
                        }
                    }
                    if (needChunk) {
                        myDedupIndex.addChunk(filePath, b, len);
                    }
                }
                if (needChunk) {
                    len = 0;  // reset len to zero for new rfp algorithm
                    offset = 0;  // read succeeding  bytes to the first place in buff
                    wantCount = m;  // read window size
                } else {
                    offset += n;  // move offset to receive new byte
                    wantCount = 1;  // read one more byte to calc next rfp or ending a new chunk
                }
            }
            if (n == -1) {  // last chunk
                myDedupIndex.addLastChunk(filePath, b, len);
            }

        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }


    void download(String filePath, String localFileName, MyDedupIndex myDedupIndex) {
        List<String> checksums = myDedupIndex.pathName2checksum.get(filePath);
        if (checksums == null) {
            System.out.println(MyDedupIndex.INDEX_FILE + " doesn't contains " + filePath);  // should not be shown
            return;
        }
        List<Chunk> chunks = checksums.stream().map(t -> myDedupIndex.checksum2chunk.get(t)).collect(Collectors.toList());
        int fileSize = chunks.stream().mapToInt(t -> t.size).sum();
        byte[] fileBytes = new byte[fileSize];
        int len = 0;
        for (Chunk chunk : chunks) {
            // TODO: inline Container.readChunkBytesFromContainer can get better performance
            byte[] bytes = myDedupIndex.container.readChunkBytesFromContainer(chunk);
            System.arraycopy(bytes,0,fileBytes,len,chunk.size);
            len += chunk.size;
        }
        try {
            Files.write(Paths.get(localFileName),fileBytes);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    void delete(String pathName, MyDedupIndex myDedupIndex) {
        myDedupIndex.delete(pathName);
    }

    void checkUploadDown(String filePathToUpload, String localFilePathDownloaded) {
        boolean isEqual = compareByMemoryMappedFiles(Paths.get(filePathToUpload), Paths.get(localFilePathDownloaded));
        if (isEqual) {
            System.out.println("upload and download passed.");
        }
    }

    public static boolean compareByMemoryMappedFiles(Path path1, Path path2)  {

        try (RandomAccessFile randomAccessFile1 = new RandomAccessFile(path1.toFile(), "r");
             RandomAccessFile randomAccessFile2 = new RandomAccessFile(path2.toFile(), "r")) {

            FileChannel ch1 = randomAccessFile1.getChannel();
            FileChannel ch2 = randomAccessFile2.getChannel();
            if (ch1.size() != ch2.size()) {
                return false;
            }
            long size = ch1.size();
            MappedByteBuffer m1 = ch1.map(FileChannel.MapMode.READ_ONLY, 0L, size);
            MappedByteBuffer m2 = ch2.map(FileChannel.MapMode.READ_ONLY, 0L, size);

            return m1.equals(m2);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return false;
    }

    List<String> writeTestFile() {
        int targetFileSize = 4*1024*1024;
        int fileCount = 1;
        int randomPatternLen = 273;
        Random random = new Random(System.currentTimeMillis());
        List<String> testFiles = new ArrayList<>(fileCount);
        File file = Paths.get(Container.DATA_DIR).toFile();
        if (!file.isDirectory()) {
            file.mkdir();
        }
        for (int i = 0; i < fileCount; i++) {
            String name = "testFile_" + System.currentTimeMillis();
            Path path = Paths.get(Container.DATA_DIR, name);
            testFiles.add(path.toString());
            byte[] b = new byte[randomPatternLen];
            random.nextBytes(b);
            try (FileOutputStream outputStream = new FileOutputStream(path.toFile())) {
                for (int j = 0; j < targetFileSize/randomPatternLen; j++) {
                    outputStream.write(b);
                }
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
         return testFiles;
    }

}
